package 多线程;

/*
线程同步
当多个线程同时运行时，线程的调度由操作系统决定，程序本身无法决定。因此，任何一个线程都有可能在任何指令处被操作系统暂停，然后在
某个时间段后继续执行。

这个时候，有个单线程模型下不存在的问题就来了：如果多个线程同时读写共享变量，会出现数据不一致的问题。

我们来看一个例子：
*/
// 多线程
/*public class 线程同步 {
    public static void main(String[] args) throws Exception {
        var add = new AddThread();
        var dec = new DecThread();
        add.start();
        dec.start();
        add.join();
        dec.join();
        System.out.println(Counter.count);
    }
}

class Counter {
    public static int count = 0;
}

class AddThread extends Thread {
    public void run() {
        for (int i=0; i<10000; i++) { Counter.count += 1; }
    }
}

class DecThread extends Thread {
    public void run() {
        for (int i=0; i<10000; i++) { Counter.count -= 1; }
    }
}*/

/*
上面的代码很简单，两个线程同时对一个int变量进行操作，一个加10000次，一个减10000次，最后结果应该是0，但是，每次运行，结
果实际上都是不一样的。

这是因为对变量进行读取和写入时，结果要正确，必须保证是原子操作。原子操作是指不能被中断的一个或一系列操作。

例如，对于语句：

n = n + 1;
看上去是一行语句，实际上对应了3条指令：

ILOAD
IADD
ISTORE
我们假设n的值是100，如果两个线程同时执行n = n + 1，得到的结果很可能不是102，而是101，原因在于：

┌───────┐    ┌───────┐
│Thread1│    │Thread2│
└───┬───┘    └───┬───┘
    │            │
    │ILOAD (100) │
    │            │ILOAD (100)
    │            │IADD
    │            │ISTORE (101)
    │IADD        │
    │ISTORE (101)│
    ▼            ▼
如果线程1在执行ILOAD后被操作系统中断，此刻如果线程2被调度执行，它执行ILOAD后获取的值仍然是100，最终结果被两个线程
的ISTORE写入后变成了101，而不是期待的102。

这说明多线程模型下，要保证逻辑正确，对共享变量进行读写时，必须保证一组指令以原子方式执行：即某一个线程执行时，其他线程必须等待：

┌───────┐     ┌───────┐
│Thread1│     │Thread2│
└───┬───┘     └───┬───┘
    │             │
    │-- lock --   │
    │ILOAD (100)  │
    │IADD         │
    │ISTORE (101) │
    │-- unlock -- │
    │             │-- lock --
    │             │ILOAD (101)
    │             │IADD
    │             │ISTORE (102)
    │             │-- unlock --
    ▼             ▼
通过加锁和解锁的操作，就能保证3条指令总是在一个线程执行期间，不会有其他线程会进入此指令区间。即使在执行期线程被操作系统中断
执行，其他线程也会因为无法获得锁导致无法进入此指令区间。只有执行线程将锁释放后，其他线程才有机会获得锁并执行。这种加锁和解
锁之间的代码块我们称之为临界区（Critical Section），任何时候临界区最多只有一个线程能执行。

可见，保证一段代码的原子性就是通过加锁和解锁实现的。Java程序使用synchronized关键字对一个对象进行加锁：

synchronized(lock) {
    n = n + 1;
}
synchronized保证了代码块在任意时刻最多只有一个线程能执行。我们把上面的代码用synchronized改写如下：
*/
/*
// 多线程
public class 线程同步 {
    public static void main(String[] args) throws Exception {
        var add = new AddThread();
        var dec = new DecThread();
        add.start();
        dec.start();
        add.join();
        dec.join();
        System.out.println(Counter1.count);
    }
}

class Counter1 {
    //public static final Object lock = new Object();
    public static int count = 0;
}

class AddThread extends Thread {
    public void run() {
        for (int i=0; i<10000; i++) {
      	  synchronized(Counter1.class) {
            //synchronized(Counter1.lock) {
                Counter1.count += 1;
            }
        }
    }
}

class DecThread extends Thread {
    public void run() {
        for (int i=0; i<10000; i++) {
            synchronized(Counter1.class) {
            //synchronized(Counter1.lock) {
                Counter1.count -= 1;
            }
        }
    }
}
*/
/*
注意到代码：

synchronized(Counter.lock) { // 获取锁
    ...
} // 释放锁
它表示用Counter.lock实例作为锁，两个线程在执行各自的synchronized(Counter.lock) { ... }代码块时，必须先获得锁，
才能进入代码块进行。执行结束后，在synchronized语句块结束会自动释放锁。这样一来，对Counter.count变量进行读写就不可能
同时进行。上述代码无论运行多少次，最终结果都是0。

使用synchronized解决了多线程同步访问共享变量的正确性问题。但是，它的缺点是带来了性能下降。因为synchronized代码块无法
并发执行。此外，加锁和解锁需要消耗一定的时间，所以，synchronized会降低程序的执行效率。

我们来概括一下如何使用synchronized：

1.找出修改共享变量的线程代码块；
2.选择一个共享实例作为锁；
3.使用synchronized(lockObject) { ... }。
在使用synchronized的时候，不必担心抛出异常。因为无论是否有异常，都会在synchronized结束处正确释放锁：

public void add(int m) {
    synchronized (obj) {
        if (m < 0) {
            throw new RuntimeException();
        }
        this.value += m;
    } // 无论有无异常，都会在此释放锁
}
我们再来看一个错误使用synchronized的例子：
*/
// 多线程
/*public class 线程同步 {
    public static void main(String[] args) throws Exception {
        var add = new AddThread();
        var dec = new DecThread();
        add.start();
        dec.start();
        add.join();
        dec.join();
        System.out.println(Counter.count);
    }
}

class Counter {
    public static final Object lock1 = new Object();
    public static final Object lock2 = new Object();
    public static int count = 0;
}

class AddThread extends Thread {
    public void run() {
        for (int i=0; i<10000; i++) {
            synchronized(Counter.lock1) {
                Counter.count += 1;
            }
        }
    }
}

class DecThread extends Thread {
    public void run() {
        for (int i=0; i<10000; i++) {
            synchronized(Counter.lock2) {
                Counter.count -= 1;
            }
        }
    }
}*/
/*
结果并不是0，这是因为两个线程各自的synchronized锁住的不是同一个对象！这使得两个线程各自都可以同时获得锁：因为JVM只保证
同一个锁在任意时刻只能被一个线程获取，但两个不同的锁在同一时刻可以被两个线程分别获取。

因此，使用synchronized的时候，获取到的是哪个锁非常重要。锁对象如果不对，代码逻辑就不对。

我们再看一个例子：
*/
// 多线程

public class 线程同步 {
   public static void main(String[] args) throws Exception {
   	//var ts=new Thread[] {new AddStudentThread(), new DecStudentThread(), new AddTeacherThread(), new DecTeacherThread()};
      Thread[] ts=new Thread[] {new AddStudentThread(), new DecStudentThread(), new AddTeacherThread(), new DecTeacherThread()};
        for (var t : ts) {
            t.start();
        }
        for (var t : ts) {
            t.join();
        }
        System.out.println(Counter1.studentCount);
        System.out.println(Counter1.teacherCount);
    }
}

class Counter1 {
    public static final Object lock = new Object();
    public static final Object studentlock = new Object();
    public static final Object teacherlock = new Object();
    public static int studentCount = 0;
    public static int teacherCount = 0;
}

class AddStudentThread extends Thread {
    public void run() {
        for (int i=0; i<10000; i++) {
      	  //synchronized(Counter1.lock) {
            synchronized(Counter1.studentlock) {
                Counter1.studentCount += 1;
            }
        }
    }
}

class DecStudentThread extends Thread {
    public void run() {
        for (int i=0; i<10000; i++) {
      	  //synchronized(Counter1.lock) {
            synchronized(Counter1.studentlock) {
                Counter1.studentCount -= 1;
            }
        }
    }
}

class AddTeacherThread extends Thread {
    public void run() {
        for (int i=0; i<10000; i++) {
      	  //synchronized(Counter1.lock) {
            synchronized(Counter1.teacherlock) {
                Counter1.teacherCount += 1;
            }
        }
    }
}

class DecTeacherThread extends Thread {
    public void run() {
        for (int i=0; i<10000; i++) {
      	  //synchronized(Counter1.lock) {
            synchronized(Counter1.teacherlock) {
                Counter1.teacherCount -= 1;
            }
        }
    }
}

/*
上述代码的4个线程对两个共享变量分别进行读写操作，但是使用的锁都是Counter.lock这一个对象，这就造成了原本可以并发执
行的Counter.studentCount += 1和Counter.teacherCount += 1，现在无法并发执行了，执行效率大大降低。实际上，
需要同步的线程可以分成两组：AddStudentThread和DecStudentThread，AddTeacherThread和DecTeacherThread，
组之间不存在竞争，因此，应该使用两个不同的锁，即：

AddStudentThread和DecStudentThread使用lockStudent锁：

synchronized(Counter.lockStudent) {
    ...
}
AddTeacherThread和DecTeacherThread使用lockTeacher锁：

synchronized(Counter.lockTeacher) {
    ...
}
这样才能最大化地提高执行效率。

不需要synchronized的操作
JVM规范定义了几种原子操作：
	•基本类型（long和double除外）赋值，例如：int n = m；
	•引用类型赋值，例如：List<String> list = anotherList。

long和double是64位数据，JVM没有明确规定64位赋值操作是不是一个原子操作，不过在x64平台的JVM是把long和double的赋值作为原子操作实现的。

单条原子操作的语句不需要同步。例如：
public void set(int m) {
    synchronized(lock) {
        this.value = m;
    }
}
就不需要同步。

对引用也是类似。例如：
public void set(String s) {
    this.value = s;
}
上述赋值语句并不需要同步。

但是，如果是多行赋值语句，就必须保证是同步操作，例如：
class Pair {
    int first;
    int last;
    public void set(int first, int last) {
        synchronized(this) {
            this.first = first;
            this.last = last;
        }
    }
}

有些时候，通过一些巧妙的转换，可以把非原子操作变为原子操作。例如，上述代码如果改造成：
class Pair {
    int[] pair;
    public void set(int first, int last) {
        int[] ps = new int[] { first, last };
        this.pair = ps;
    }
}
就不再需要同步，因为this.pair = ps是引用赋值的原子操作。而语句：

int[] ps = new int[] { first, last };
这里的ps是方法内部定义的局部变量，每个线程都会有各自的局部变量，互不影响，并且互不可见，并不需要同步。

小结
多线程同时读写共享变量时，会造成逻辑错误，因此需要通过synchronized同步；

同步的本质就是给指定对象加锁，加锁后才能继续执行后续代码；

注意加锁对象必须是同一个实例；

对JVM定义的单个原子操作不需要同步。
*/
/*
一、synchronized基本使用
上篇文章详细讲解了volatile关键字，我们知道volatile关键字可以保证共享变量的可见性和有序性，但并不能保证原子性。如果既想保证共享变量的可见性和有序性，又想保
证原子性，那么synchronized关键字是一个不错的选择。
synchronized的使用很简单，可以用它来修饰实例方法和静态方法，也可以用来修饰代码块。值的注意的是synchronized是一个对象锁，也就是它锁的是一个对象。因此，无
论使用哪一种方法，synchronized都需要有一个锁对象
1.修饰实例方法
synchronized修饰实例方法只需要在方法上加上synchronized关键字即可。
public synchronized void add(){
       i++;
}

此时，synchronized加锁的对象就是这个方法所在实例的本身。
2.修饰静态方法
synchronized修饰静态方法的使用与实例方法并无差别，在静态方法上加上synchronized关键字即可
public static synchronized void add(){
       i++;
}

此时，synchronized加锁的对象为当前静态方法所在类的Class对象。
3.修饰代码块
synchronized修饰代码块需要传入一个对象。
public void add() {
    synchronized (this) {
        i++;
    }
}

很明显，此时synchronized加锁对象即为传入的这个对象实例。
到这里不是道你是否有个疑问，synchronized关键字是如何对一个对象加锁实现代码同步的呢？如果想弄清楚，那就不得不先了解一下Java对象的对象头了。
二、Java对象头与Monitor对象
在JVM中，对象在内存中存储的布局可以分为三个区域，分别是对象头、实例数据以及填充数据。

-实例数据 存放类的属性数据信息，包括父类的属性信息，这部分内存按4字节对齐。
-填充数据 由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的，仅仅是为了字节对齐。
-对象头 在HotSpot虚拟机中，对象头又被分为两部分，分别为：Mark Word(标记字段)、Class Pointer(类型指针)。如果是数组，那么还会有数组长度。对象头是
本章内容的重点，下边详细讨论。

HotSpot虚拟机的一般对象头（Header）包括两部分信息：“Mark Word”、“Class Pointer”，数组类对象还包括“Array Length”。

-Mark Word：存储对象自身的运行时数据
-Class Pointer：对象指向它的类元数据的指针，虚拟机通过这个指针来确定这个对象是哪个类的实例。
-Array Length：如果是数组对象，则 Array Length 存储数组的长度。
Tip：
-对齐填充（ Padding ）：Java对象占用空间是8字节对齐的，即所有Java对象占用bytes数必须是8的整数倍。若Java对象的bytes数不是8的整数倍，则会填充一些字
节，让Java对象占用的空间是8的整数倍。

1.对象头
由于Java面向对象的思想，在JVM中需要大量存储对象，存储时为了实现一些额外的功能，需要在对象中添加一些标记字段用于增强对象功能，这些标记字段组成了对象头。
在对象头的Mark Word中主要存储了对象自身的运行时数据，例如哈希码、GC分代年龄、锁状态、线程持有的锁、偏向线程ID以及偏向时间戳等。同时，Mark Word也记录了
对象和锁有关的信息。
在HotSpot虚拟机中，对象在内存中存储的布局可以分为3块区域：对象头（Header），实例数据（Instance Data）和对齐填充（Padding）。
也就是说 JAVA对象 = 对象头 + 实例数据 + 对象填充。

其中，对象头由两部分组成，一部分用于存储自身的运行时数据，称之为 Mark Word，另外一部分是类型指针，及对象指向它的类元数据的指针。

对象头 = Mark Word + 类型指针
（未开启指针压缩的情况下）
在32位系统中，Mark Word = 4 bytes = 32 bits，对象头 = 8 bytes = 64 bits；
在64位系统中，Mark Word = 8 bytes = 64 bits ，对象头 = 16 bytes = 128bits；

bytes 是字节，bits 是位。所以说，在32位JVM虚拟机系统中，Mark Word部分，占了4个字节，Klass Word部分也占了4个字节，所以，对象头大小为8个字节。
在64位JVM虚拟机系统中，Mark Word部分，占了8个字节，Klass Word部分也占了8个字节，所以，对象头大小为16个字节。

【32位虚拟机对象头】
32位虚拟机普通对象的对象头

|-----------------------------------------------------------|
|                    Object Header (64 bits)                |
|---------------------------------|-------------------------|
|             Mark Word (32 bits) | Klass Word (32 bits)    |
|---------------------------------|-------------------------|

32位虚拟机数组对象的对象头

|---------------------------------------------------------------------------------|
|                                  Object Header (96 bits)                        |
|--------------------------------|-----------------------|------------------------|
|       Mark Word(32bits)        | Klass Word(32bits)    | array length(32bits)   |
|--------------------------------|-----------------------|------------------------|

32位虚拟机对象头详情如下
|-------------------------------------------------------|--------------------|
|                  Mark Word (32 bits)                  |       State        |
|-------------------------------------------------------|--------------------|
| identity_hashcode:25 | age:4 | biased_lock:1 | lock:2 |       Normal       |
|-------------------------------------------------------|--------------------|
|  thread:23 | epoch:2 | age:4 | biased_lock:1 | lock:2 |       Biased       |
|-------------------------------------------------------|--------------------|
|               ptr_to_lock_record:30          | lock:2 | Lightweight Locked |
|-------------------------------------------------------|--------------------|
|               ptr_to_heavyweight_monitor:30  | lock:2 | Heavyweight Locked |
|-------------------------------------------------------|--------------------|
|                                              | lock:2 |    Marked for GC   |
|-------------------------------------------------------|--------------------|
|-----------------------------------------------------------------------------------------------------------------|
|                                             Object Header(64bits)                                               |
|-----------------------------------------------------------------------------------------------------------------|
|                       Mark Word(32bits)                           |  Klass Word(32bits)    |      State         |
|-----------------------------------------------------------------------------------------------------------------|
|     hashcode:25                      | age:4 | biased_lock:0 | 01 | OOP to metadata object |      Nomal         |无锁状态
|-----------------------------------------------------------------------------------------------------------------|
|     thread:23              | epoch:2 | age:4 | biased_lock:1 | 01 | OOP to metadata object |      Biased        |偏向锁
|-----------------------------------------------------------------------------------------------------------------|
|     ptr_to_lock_record:30                                    | 00 | OOP to metadata object | Lightweight Locked |轻量级锁
|-----------------------------------------------------------------------------------------------------------------|
|     ptr_to_heavyweight_monitor:30                            | 10 | OOP to metadata object | Heavyweight Locked |重量级锁
|-----------------------------------------------------------------------------------------------------------------|
|                                                              | 11 | OOP to metadata object |    Marked for GC   |GC标记
|-----------------------------------------------------------------------------------------------------------------|
																					  ↑
                                                               锁状态
从图中可以清楚的看到，Mark Word中有2bit的数据用来标记锁的状态。无锁状态和偏向锁标记位为01，轻量级锁的状态为00，重量级锁的状态为10。

-当对象为偏向锁时，Mark Word存储了偏向线程的ID；
-当状态为轻量级锁时，Mark Word存储了指向线程栈中Lock Record的指针；
-当状态为重量级锁时，Mark Word存储了指向堆中的Monitor对象的指针。


【64位虚拟机对象头】
|------------------------------------------------------------------------------|--------------------|
|                                  Mark Word (64 bits)                         |       State        |
|------------------------------------------------------------------------------|--------------------|
| unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 |       Normal       |
|------------------------------------------------------------------------------|--------------------|
| thread:54 |       epoch:2        | unused:1 | age:4 | biased_lock:1 | lock:2 |       Biased       |
|------------------------------------------------------------------------------|--------------------|
|                       ptr_to_lock_record:62                         | lock:2 | Lightweight Locked |
|------------------------------------------------------------------------------|--------------------|
|                     ptr_to_heavyweight_monitor:62                   | lock:2 | Heavyweight Locked |
|------------------------------------------------------------------------------|--------------------|
|                                                                     | lock:2 |    Marked for GC   |
|------------------------------------------------------------------------------|--------------------|

|-----------------------------------------------------------------------------------------------------------------|
|                                             Object Header(128bits)                                              |
|-----------------------------------------------------------------------------------------------------------------|
|                                   Mark Word(64bits)               |  Klass Word(64bits)    |      State         |
|-----------------------------------------------------------------------------------------------------------------|
|    unused:25|identity_hashcode:31|unused:1|age:4|biase_lock:0| 01 | OOP to metadata object |      Nomal         |
|-----------------------------------------------------------------------------------------------------------------|
|    thread:54|      epoch:2       |unused:1|age:4|biase_lock:1| 01 | OOP to metadata object |      Biased        |
|-----------------------------------------------------------------------------------------------------------------|
|                        ptr_to_lock_record:62                 | 00 | OOP to metadata object | Lightweight Locked |
|-----------------------------------------------------------------------------------------------------------------|
|                       ptr_to_heavyweight_monitor:62          | 10 | OOP to metadata object | Heavyweight Locked |
|-----------------------------------------------------------------------------------------------------------------|
|                                                              | 11 | OOP to metadata object |    Marked for GC   |
|-----------------------------------------------------------------------------------------------------------------|


lock:2位的锁状态标记位，由于希望用尽可能少的二进制位表示尽可能多的信息，所以设置了lock标记。该标记的值不同，整个mark word表示的含义不同。

biased_lock	lock	状态
0	         01	   无锁
1	         01	   偏向锁
0	         00	   轻量级锁
0	         10	   重量级锁
0	         11	   GC标记
biased_lock：对象是否启用偏向锁标记，只占1个二进制位。为1时表示对象启用偏向锁，为0时表示对象没有偏向锁。
age：4位的Java对象年龄。在GC中，如果对象在Survivor区复制一次，年龄增加1。当对象达到设定的阈值时，将会晋升到老年代。默认情况下，并行GC的年龄阈值为15，
    并发GC的年龄阈值为6。由于age只有4位，所以最大值为15，这就是-XX:MaxTenuringThreshold选项最大值为15的原因。
identity_hashcode：25位的对象标识Hash码，采用延迟加载技术。调用方法System.identityHashCode()计算，并会将结果写到该对象头中。当对象被锁定时，该
                  值会移动到管程Monitor中。
                  对象的hashcode，如果对象没有重写hashcode()方法，那么通过System.identityHashCode()方法获取。采用延迟加载技术，不会主动计算，
                  但是一旦生成了hashcode，JVM会将其记录在 Mark Word 中。
thread：持有偏向锁的线程ID。
epoch：偏向时间戳。
ptr_to_lock_record：指向栈中锁记录的指针。
ptr_to_heavyweight_monitor：指向管程Monitor的指针。

如何查看对象头
查看对象头，就需要用到借助JOL工具。

<dependency>
     <groupId>org.openjdk.jol</groupId>
     <artifactId>jol-core</artifactId>
     <version>0.10</version>
</dependency>


import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.vm.VM;
import static java.lang.System.out;

public class TestBiased {
    public static void main(String[] args) {
        Dog d= new Dog();
        //打印JVM的详细信息
        out.println(VM.current().details());
        //打印对应的对象头信息
        out.println(ClassLayout.parseInstance(a).toPrintable());
    }
}

class Dog {
}


打印，大概如下。

21:46:08.204 c.TestBiased [main] - # Running 64-bit HotSpot VM.
# Objects are 8 bytes aligned.
# Field sizes by type: 8, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
# Array element sizes: 8, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]

21:46:08.215 TestBiased [main] - cn.itcast.test.Dog object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                 VALUE
      0     4        (object header)             01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)             00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)             50 c8 e1 3f (01010000 11001000 11100001 00111111) (1071761488)
     12     4        (object header)             71 01 00 00 (01110001 00000001 00000000 00000000) (369)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

如果打印不一样，可以加VM options参数-XX:-UseCompressedOops，关闭指针压缩。

【class pointer】
这一部分用于存储对象的类型指针，该指针指向它的类元数据，JVM通过这个指针确定对象是哪个类的实例。该指针的位长度为JVM的一个字大小，即32位的JVM为32位，64位
的JVM为64位。
如果应用的对象过多，使用64位的指针将浪费大量内存，统计而言，64位的JVM将会比32位的JVM多耗费50%的内存。为了节约内存可以使用选项+UseCompressedOops开
启指针压缩，其中，oop即ordinary object pointer普通对象指针。开启该选项后，下列指针将压缩至32位：

每个Class的属性指针（即静态变量）
每个对象的属性指针（即对象变量）
普通对象数组的每个元素指针
当然，也不是所有的指针都会压缩，一些特殊类型的指针JVM不会优化，比如指向PermGen的Class对象指针(JDK8中指向元空间的Class对象指针)、本地变量、堆栈元
素、入参、返回值和NULL指针等。

【array length】
如果对象是一个数组，那么对象头还需要有额外的空间用于存储数组的长度，这部分数据的长度也随着JVM架构的不同而不同：32位的JVM上，长度为32位；64位JVM则为64位。
64位JVM如果开启+UseCompressedOops选项，该区域长度也将由64位压缩至32位。




当对象被synchronized关键字当成同步锁时，和锁相关的一系列操作都与Mark Word有关。由于在JDK1.6版本中对synchronized进行了锁优化，引入了偏向锁和轻量级
锁（关于锁优化后边详情讨论）。Mark Word在不同锁状态下存储的内容有所不同。我们以32位JVM中对象头的存储内容如下图所示。


从图中可以清楚的看到，Mark Word中有2bit的数据用来标记锁的状态。无锁状态和偏向锁标记位为01，轻量级锁的状态为00，重量级锁的状态为10。

当对象为偏向锁时，Mark Word存储了偏向线程的ID；
当状态为轻量级锁时，Mark Word存储了指向线程栈中Lock Record的指针；
当状态为重量级锁时，Mark Word存储了指向堆中的Monitor对象的指针。

当前我们只讨论重量级锁，因为重量级锁相当于对synchronized优化之前的状态。关于偏向锁和轻量级锁在后边锁优化章节中详细讲解。
可以看到，当为重量级锁时，对象头的MarkWord中存储了指向Monitor对象的指针。那么Monitor又是什么呢？
2.Monitor对象
Monitor对象被称为管程或者监视器锁。在Java中，每一个对象实例都会关联一个Monitor对象。这个Monitor对象既可以与对象一起创建销毁，也可以在线程试图获取对象锁
时自动生成。当这个Monitor对象被线程持有后，它便处于锁定状态。
在HotSpot虚拟机中，Monitor是由ObjectMonitor实现的,它是一个使用C++实现的类，主要数据结构如下：
ObjectMonitor() {
    _header       = NULL;
    _count        = 0; //记录个数
    _waiters      = 0,
    _recursions   = 0;  // 线程重入次数
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; // 调用wait方法后的线程会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ; // 阻塞队列，线程被唤醒后根据决策判读是放入cxq还是EntryList
    FreeNext      = NULL ;
    _EntryList    = NULL ; // 没有抢到锁的线程会被放到这个队列
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

ObjectMonitor中有五个重要部分，分别为_ower,_WaitSet,_cxq,_EntryList和count。

◆ _ower 用来指向持有monitor的线程，它的初始值为NULL,表示当前没有任何线程持有monitor。当一个线程成功持有该锁之后会保存线程的ID标识，等到线程释放锁
	后_ower又会被重置为NULL;
◆ _WaitSet 调用了锁对象的wait方法后的线程会被加入到这个队列中；
◆ _cxq  是一个阻塞队列，线程被唤醒后根据决策判读是放入cxq还是EntryList;
◆ _EntryList 没有抢到锁的线程会被放到这个队列；
◆ count 用于记录线程获取锁的次数，成功获取到锁后count会加1，释放锁时count减1。

如果线程获取到对象的monitor后，就会将monitor中的ower设置为该线程的ID，同时monitor中的count进行加1.
如果调用锁对象的wait()方法，线程会释放当前持有的monitor，并将owner变量重置为NULL，且count减1,同时该线程会进入到_WaitSet集合中等待被唤醒。
另外_WaitSet，_cxq与_EntryList都是链表结构的队列，存放的是封装了线程的ObjectWaiter对象。如果不深入虚拟机查看相关源码很难理解这几个队列的作用，关于源
码会在后边系列文章中分析。这里我简单说下它们之间的关系，如下：
在多条线程竞争monitor锁的时候，所有没有竞争到锁的线程会被封装成ObjectWaiter并加入_EntryList队列。
当一个已经获取到锁的线程，调用锁对象的wait方法后，线程也会被封装成一个ObjectWaiter并加入到_WaitSet队列中。
当调用锁对象的notify方法后，会根据不同的情况来决定是将_WaitSet集合中的元素转移到_cxq队列还是_EntryList队列。
等到获得锁的线程释放锁后，又会根据条件来执行_EntryList中的线程或者将_cxq转移到_EntryList中再执行_EntryList中的线程。
所以，可以看得出来，_WaitSet存放的是处于WAITING状态等待被唤醒的线程。而_EntryList队列中存放的是等待锁的BLOCKED状态。_cxq队列仅仅是临时存放，最终还是
会被转移到_EntryList中等待获取锁。
了解了对象头和Monitor，那么synchronized关键字到底是如何做到与monitor关联的呢？
三、synchronized底层实现原理
在Java代码中，我们只是使用了synchronized关键字就实现了同步效果。那他到底是怎么做到的呢？这就需要我们通过javap工具来反汇编出字节指令一探究竟了。
1.同步代码块
通过javap -v来反汇编下面的一段代码。
public void add() {
    synchronized (this) {
        i++;
    }
}

可以得到如下的字节码指令：
public class com.zhangpan.text.TestSync {
  public com.zhangpan.text.TestSync();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public void add();
    Code:
       0: aload_0
       1: dup
       2: astore_1
       3: monitorenter    // synchronized关键字的入口
       4: getstatic     #2                  // Field i:I
       7: iconst_1
       8: iadd
       9: putstatic     #2                  // Field i:I
      12: aload_1
      13: monitorexit  // synchronized关键字的出口
      14: goto          22
      17: astore_2
      18: aload_1
      19: monitorexit // synchronized关键字的出口
      20: aload_2
      21: athrow
      22: return
    Exception table:
       from    to  target type
           4    14    17   any
          17    20    17   any
}

从字节码指令中可以看到add方法的第3条指令处和第13、19条指令处分别有monitorenter和moniterexit两条指令。另外第4、7、8、9、13这几条指令其实就是i++的指
令。由此可以得出在字节码中会在同步代码块的入口和出口加上monitorenter和moniterexit指令。当执行到monitorenter指令时，线程就会去尝试获取该对象对应
的Monitor的所有权，即尝试获得该对象的锁。
当该对象的 monitor 的计数器count为0时，那线程可以成功取得 monitor，并将计数器值设置为 1，取锁成功。如果当前线程已经拥有该对象monitor的持有权，那它
可以重入这个 monitor ，计数器的值也会加 1。而当执行monitorexit指令时，锁的计数器会减1。
倘若其他线程已经拥有monitor 的所有权，那么当前线程获取锁失败将被阻塞并进入到_EntryList中，直到等待的锁被释放为止。也就是说，当所有相应的monitorexit指
令都被执行，计数器的值减为0，执行线程将释放 monitor(锁)，其他线程才有机会持有 monitor 。
2.同步方法的实现
同步方法的字节码指令与同步代码块的字节指令有所差异。我们先来通过javap -v查看下面代码的字节码指令。
public synchronized void add(){
       i++;
}

反汇编后可得到如下的字节指令
 public synchronized void add();
    descriptor: ()V
    flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #2                  // Field i:I
         5: iconst_1
         6: iadd
         7: putfield      #2                  // Field i:I
        10: return
      LineNumberTable:
        line 5: 0
        line 6: 10

可以看到这里并没有monitorenter和moniterexit两条指令，而是在方法的flag上加入了ACC_SYNCHRONIZED的标记位。这其实也容易理解，因为整个方法都是同步代码，因此就不需要标记同步代码的入口和出口了。当线程线程执行到这个方法时会判断是否有这个ACC_SYNCHRONIZED标志，如果有的话则会尝试获取monitor对象锁。执行步骤与同步代码块一致，这里就不再赘述了。
四、重量级锁存在性能问题
在Linux系统架构中可以分为用户空间和内核，我们的程序都运行在用户空间，进入用户运行状态就是所谓的用户态。在用户态可能会涉及到某些操作如I/O调用，就会进入内核中运行，此时进程就被称为内核运行态，简称内核态。


内核： 本质上可以理解为一种软件，控制计算机的硬件资源，并提供上层应用程序运行的环境。
用户空间： 上层应用程序活动的空间。应用程序的执行必须依托于内核提供的资源，包括CPU资源、存储资源、I/O资源等。
系统调用： 为了使上层应用能够访问到这些资源，内核必须为上层应用提供访问的接口：即系统调用。

上边我们已经提到了使用monitor是重量级锁的加锁方式。在objectMonitor.cpp中会涉及到Atomic::cmpxchg_ptr，Atomic::inc_ptr等内核函数，
执行同步代码块，没有竞争到锁的对象会park()被挂起，竞争到锁的线程会unpark()唤醒。这个时候就会存在操作系统用户态和内核态的转换，这种切换会消耗大量的系统资源。试想，如果程序中存在大量的锁竞争，那么会引起程序频繁的在用户态和内核态进行切换，严重影响到程序的性能。这也是为什么说synchronized效率低的原因
为了解决这一问题，在JDK1.6中引入了偏向锁和轻量级锁来优化synchronized。
五、synchronized锁优化
JDK1.6中引入偏向锁和轻量级锁对synchronized进行优化。此时的synchronized一共存在四个状态：无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。锁着锁竞争激烈程度，锁的状态会出现一个升级的过程。即可以从偏向锁升级到轻量级锁，再升级到重量级锁。锁升级的过程是单向不可逆的，即一旦升级为重量级锁就不会再出现降级的情况。
1.几种锁状态
接下来我们来详细的认识一下这几种锁状态。
1).偏向锁
经研究发现，在大多数情况下锁不仅不存在多线程竞争关系，而且大多数情况都是被同一线程多次获得。因此，为了减少同一线程获取锁的代价而引入了偏向锁的概念。
偏向锁的核心思想是，如果一个线程获得了锁，那么锁就进入偏向模式，此时Mark Word的结构也变为偏向锁结构，即将对象头中Mark Word的第30bit的值改为1，并且
在Mark Word中记录该线程的ID。当这个线程再次请求锁时，无需再做任何同步操作，即可获取锁的过程，这样就省去了大量有关锁申请的操作，从而也就提升了程序的性能。
所以，对于没有锁竞争的场合，偏向锁有很好的优化效果，毕竟极有可能连续多次是同一个线程申请相同的锁。
但是，对于锁竞争比较激烈的情况，偏向锁就有问题了。因为每次申请锁的都可能是不同线程。这种情况使用偏向锁就会得不偿失，此时就会升级为轻量级锁。
2).轻量级锁
轻量级锁优化性能的依据是对于大部分的锁，在整个同步生命周期内都不存在竞争。 当升级为轻量级锁之后，MarkWord的结构也会随之变为轻量级锁结构。JVM会利用CAS尝试把
对象原本的Mark Word 更新为Lock Record的指针，成功就说明加锁成功，改变锁标志位为00，然后执行相关同步操作。
轻量级锁所适应的场景是线程交替执行同步块的场合，如果存在同一时间访问同一锁的场合，就会导致轻量级锁就会失效，进而膨胀为重量级锁。
3).自旋锁
轻量级锁失败后，虚拟机为了避免线程真实地在操作系统层面挂起，还会进行一项称为自旋锁的优化手段。
自旋锁是基于在大多数情况下，线程持有锁的时间都不会太长。如果直接挂起操作系统层面的线程可能会得不偿失，毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态，
这个状态之间的转换需要相对比较长的时间，时间成本相对较高，因此自旋锁会假设在不久将来，当前的线程可以获得锁，因此虚拟机会让当前想要获取锁的线程做几个空循环(这也
是称为自旋的原因)，不断的尝试获取锁。空循环一般不会执行太多次，可能是50个循环或100循环，在经过若干次循环后，如果得到锁，就顺利进入同步代码。如果还不能获得锁，
那就会将线程在操作系统层面挂起，即进入到重量级锁。
这就是自旋锁的优化方式，这种方式确实也是可以提升效率的。
2.synchronized锁升级过程
在了解了jdk1.6引入的这几种锁之后，我们来详细的看一下synchronized是怎么一步步进行锁升级的。
(1) 当没有被当成锁时，这就是一个普通的对象，Mark Word记录对象的HashCode，锁标志位是01，是否偏向锁那一位是0;
(2) 当对象被当做同步锁并有一个线程A抢到了锁时，锁标志位还是01，但是否偏向锁那一位改成1，前23bit记录抢到锁的线程id，表示进入偏向锁状态;
(3) 当线程A再次试图来获得锁时，JVM发现同步锁对象的标志位是01，是否偏向锁是1，也就是偏向状态，Mark Word中记录的线程id就是线程A自己的id，表示线程A已经获得
    了这个偏向锁，可以执行同步中的代码;
(4) 当线程B试图获得这个锁时，JVM发现同步锁处于偏向状态，但是Mark Word中的线程id记录的不是B，那么线程B会先用CAS操作试图获得锁，这里的获得锁操作是有可能成
	功的，因为线程A一般不会自动释放偏向锁。如果抢锁成功，就把Mark Word里的线程id改为线程B的id，代表线程B获得了这个偏向锁，可以执行同步代码。如果抢锁失败，
	则继续执行步骤5;
(5) 偏向锁状态抢锁失败，代表当前锁有一定的竞争，偏向锁将升级为轻量级锁。JVM会在当前线程的线程栈中开辟一块单独的空间，里面保存指向对象锁Mark Word的指针，同
	时在对象锁Mark Word中保存指向这片空间的指针。上述两个保存操作都是CAS操作，如果保存成功，代表线程抢到了同步锁，就把Mark Word中的锁标志位改成00，可以
	执行同步代码。如果保存失败，表示抢锁失败，竞争太激烈，继续执行步骤6;
(6) 轻量级锁抢锁失败，JVM会使用自旋锁，自旋锁不是一个锁状态，只是代表不断的重试，尝试抢锁。从JDK1.7开始，自旋锁默认启用，自旋次数由JVM决定。如果抢锁成功则
	执行同步代码，如果失败则继续执行步骤7;
(7) 自旋锁重试之后如果抢锁依然失败，同步锁会升级至重量级锁，锁标志位改为10。在这个状态下，未抢到锁的线程都会被阻塞。
六、总结
synchronized关键字的使用可以说非常简单，但是想要完全搞懂synchronized实际上并没有那么容易。因为它涉及到很多虚拟机底层的知识。同时，还要了解JDK1.6中
对synchronized针对性的优化，其中牵扯到的东西又很多。比如，本篇文章并没有讲解什么是CAS，如果你不懂CAS，就很难理解锁升级的过程。需要不懂读者自行去查阅相
关资料。本篇文章对于synchronized的讲解相对来说还是很全面的。希望你看完能有所收获。

*/

/*
一、synchronized 代码块
public class SyncTest {

    public static void main(String[] args) {

        synchronized (SyncTest.class) {
        
        }
    }
}

同步代码块中，锁为syncronized括号中配置的对象。

二、synchronized 方法
public class SyncTest {//普通同步方法，锁是当前实例对象this。

    public synchronized void fun(){
    
    }
}

public class SyncTest {//静态同步方法，锁是当前类的Class对象。

    public synchronized static void fun(){
    
    }
}

【同步代码块与同步方法的使用注意事项】
1.只有同步代码块与同步方法的锁对象是同一个对象对象时，才能保证同一时刻，只有一个线程访问共享资源。
class SyncWrong implements Runnable {
    private static int i = 0;
    private synchronized void add() {
        i++;
    }

    @Override
    public void run() {
        for (int j = 0; j < 1000000; j++) {
            add();
        }
    }

    public static void main(String[] args) throws Exception {

        Thread t1 = new Thread(new SyncWrong());
        Thread t2 = new Thread(new SyncWrong());

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println(i);
    }
}

可以看到，虽然我们对 add方法加了锁，但依然不能保证输出结果的正确性。

这段代码违反了上述规则，add方法是普通同步方法，因此该方法的锁对象是当前实例对象this，主函数中的 t1、t2 线程传入了不同的SyncWrong 对象，因
此，synchronized 锁对象不是同一个，自然就无法达到同步的目的。解决办法就是让 synchronized 锁对象是同一个。

方法一：只创建一个SyncWrong对象。

public static void main(String[] args) throws Exception {

		SyncWrong wrong = new SyncWrong();
		
        Thread t1 = new Thread(wrong);
        Thread t2 = new Thread(wrong);

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println(i);
 }

此时，synchronized 锁对象 就是 wrong 这个对象了。

方法二：将add方法改为静态方法。

private synchronized static void add() {
    	i++;
}

静态同步方法的锁对象是当前类的Class对象，类被装载后，在内存中有且仅有一个该类的Class对象，因此 可以保证静态同步方法的锁对象是同一个。

2.每个访问共享资源的代码都必须被同步。
class Resource{

    private static int i = 0;

    public synchronized void add() {
        i++;
    }
    
    public void reduce() {
        i--;
    }
}

对于上述代码，add 和 reduce 都对共享资源 i 进行了处理，你必须同步这两个方法（将 reduce 改造为同步方法）。如果不这样做，那么reduce()方法就会完全忽略
这个对象锁，也就是在当前线程获得到锁，执行add操作时，其他线程可以随意的调用 reduce方法，从而导致并发修改的问题。

3.私有化共享资源。
共享资源一般以对象形式存在于内存中，因此要控制共享资源的访问。

class Resource{

    public static int i = 0;

    public synchronized void add() {
        i++;
    }
    
	public synchronized void get() {
        return i;
    }
}

上述代码的问题在于任何线程都可以通过 Resource.i 来获取共享变量的值，若当前线程获得锁，正在执行add同步方法的内容，其他线程通过 Resource.i 则会忽略锁，可
能获取到中间结果。正确的方法是将 共享资源 i 私有化（用 private 修饰）, 然后提供一个被同步的获取方式。
*/
/*
synchronized同步锁用法及作用范围

Java 中的 synchronized 关键字可以在多线程环境下用来作为线程安全的同步锁。本文主要对 synchronized 的作用，以及其有效范围进行讨论。
Java中的对象锁和类锁：java的对象锁和类锁在锁的概念上基本上和内置锁是一致的，但是，两个锁实际是有很大的区别的，对象锁是用于对象实例方法，或者一个对象实例上的，
类锁是用于类的静态方法或者一个类的class对象上的。我们知道，类的对象实例可以有很多个，但是每个类只有一个class对象，所以不同对象实例的对象锁是互不干扰的，但是
每个类只有一个类锁。但是有一点必须注意的是，其实类锁只是一个概念上的东西，并不是真实存在的，它只是用来帮助我们理解锁定实例方法和静态方法的区别的。

synchronized 关键字主要有以下几种用法：
- 非静态方法的同步；
- 静态方法的同步；
- 代码块。

下面分对象锁和类锁来分别说明 synchronized 用法：

【对象锁】
非静态方法使用 synchronized 修饰的写法，修饰实例方法时，锁定的是当前对象：

    public synchronized void test(){
        // TODO
    }

代码块使用 synchronized 修饰的写法，使用代码块，如果传入的参数是 this，那么锁定的也是当前的对象：

    public void test(){
        synchronized (this) {
            // TODO
        }
    }

下面通过例子来说明对象锁：
定义一个类，方法如下，将 count 自减，从 5 到 0：

public class TestSynchronized {

    public synchronized void minus() {
        int count = 5;
        for (int i = 0; i < 5; i++) {
            count--;
            System.out.println(Thread.currentThread().getName() + " - " + count);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
            }
        }
    }

}

测试调用方法如下：

public class Run {

    public static void main(String[] args) {

        final TestSynchronized test = new TestSynchronized();

        Thread thread1 = new Thread(new Runnable() {

            @Override
            public void run() {
                test.minus();
            }
        });

        Thread thread2 = new Thread(new Runnable() {

            @Override
            public void run() {
                test.minus();
            }
        });

        thread1.start();
        thread2.start();

    }

}

两个线程 thread1 和 thread2，同时访问对象的方法，由于该方法是 synchronized 关键字修饰的，那么这两个线程都需要获得该对象锁，一个获得后另一个线程必须
等待。所以我们可以猜测运行结果应该是，一个线程执行完毕后，另一个线程才开始执行，运行例子，输出打印结果如下：

Thread-0 - 4
Thread-0 - 3
Thread-0 - 2
Thread-0 - 1
Thread-0 - 0
Thread-1 - 4
Thread-1 - 3
Thread-1 - 2
Thread-1 - 1
Thread-1 - 0

（另：thread1 和 thread2 谁先执行并不一定）
本例对于对象锁进行了基础的解释。但是对象锁的范围是怎样的，对象的某个同步方法被一个线程访问后，其他线程能不能访问该对象的其他同步方法，以及是否可以访问对象的其他
非同步方法呢，下面对两种进行验证：

对两个同步方法两个线程的验证：
修改类如下，加入 minus2() 方法，和 minus() 方法一样：

package com.test.run;

public class TestSynchronized {

    public synchronized void minus() {
        int count = 5;
        for (int i = 0; i < 5; i++) {
            count--;
            System.out.println(Thread.currentThread().getName() + " - " + count);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
            }
        }
    }

    public synchronized void minus2() {
        int count = 5;
        for (int i = 0; i < 5; i++) {
            count--;
            System.out.println(Thread.currentThread().getName() + " - " + count);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
            }
        }
    }

}

测试调用如下，两个线程访问不同的方法：

public class Run {

    public static void main(String[] args) {

        final TestSynchronized test = new TestSynchronized();

        Thread thread1 = new Thread(new Runnable() {

            @Override
            public void run() {
                test.minus();
            }
        });

        Thread thread2 = new Thread(new Runnable() {

            @Override
            public void run() {
                test.minus2();
            }
        });

        thread1.start();
        thread2.start();
    }

}

输出结果如下：

Thread-0 - 4
Thread-0 - 3
Thread-0 - 2
Thread-0 - 1
Thread-0 - 0
Thread-1 - 4
Thread-1 - 3
Thread-1 - 2
Thread-1 - 1
Thread-1 - 0

可以看到，某个线程得到了对象锁之后，该对象的其他同步方法是锁定的，其他线程是无法访问的。
下面看是否能访问非同步方法：
修改类代码如下，将 minus2() 的 synchronized 修饰去掉，代码如下：

public class TestSynchronized {

    public synchronized void minus() {
        int count = 5;
        for (int i = 0; i < 5; i++) {
            count--;
            System.out.println(Thread.currentThread().getName() + " - " + count);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
            }
        }
    }

    public void minus2() {
        int count = 5;
        for (int i = 0; i < 5; i++) {
            count--;
            System.out.println(Thread.currentThread().getName() + " - " + count);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
            }
        }
    }

}


测试调用的类不变，如下：

public class Run2 {

    public static void main(String[] args) {

        final TestSynchronized test = new TestSynchronized();

        Thread thread1 = new Thread(new Runnable() {

            @Override
            public void run() {
                test.minus();
            }
        });

        Thread thread2 = new Thread(new Runnable() {

            @Override
            public void run() {
                test.minus2();
            }
        });

        thread1.start();
        thread2.start();

    }

}


执行结果如下：

Thread-1 - 4
Thread-0 - 4
Thread-1 - 3
Thread-0 - 3
Thread-1 - 2
Thread-0 - 2
Thread-1 - 1
Thread-0 - 1
Thread-1 - 0
Thread-0 - 0

可以看到，结果是交替的，说明线程是交替执行的，说明如果某个线程得到了对象锁，但是另一个线程还是可以访问没有进行同步的方法或者代码。进行了同步的方法（加锁方法）和没有进行同步的方法（普通方法）是互不影响的，一个线程进入了同步方法，得到了对象锁，其他线程还是可以访问那些没有同步的方法（普通方法）。当获取到与对象关联的内置锁时，并不能阻止其他线程访问该对象，当某个线程获得对象的锁之后，只能阻止其他线程获得同一个锁。

类锁
类锁需要 synchronized 来修饰静态 static 方法，写法如下：

    public static synchronized void test(){
        // TODO
    }

或者使用代码块，需引用当前的类：

    public static void test(){
        synchronized (TestSynchronized.class) {
            // TODO
        }
    }

举例说明类锁的作用：

public class TestSynchronized {

    public static synchronized void minus() {
        int count = 5;
        for (int i = 0; i < 5; i++) {
            count--;
            System.out.println(Thread.currentThread().getName() + " - " + count);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
            }
        }
    }

}

测试调用类如下：

public class Run {

    public static void main(String[] args) {

        Thread thread1 = new Thread(new Runnable() {

            @Override
            public void run() {
                TestSynchronized.minus();
            }
        });

        Thread thread2 = new Thread(new Runnable() {

            @Override
            public void run() {
                TestSynchronized.minus();
            }
        });

        thread1.start();
        thread2.start();

    }

}

输出结果如下：

Thread-0 - 4
Thread-0 - 3
Thread-0 - 2
Thread-0 - 1
Thread-0 - 0
Thread-1 - 4
Thread-1 - 3
Thread-1 - 2
Thread-1 - 1
Thread-1 - 0

可以看到，类锁和对象锁其实是一样的，由于静态方法是类所有对象共用的，所以进行同步后，该静态方法的锁也是所有对象唯一的。每次只能有一个线程来访问对象的该非静态同步方法。
类锁的作用和对象锁类似，但是作用范围是否和对象锁一致呢，下面看对象锁和类锁是否等同：
修改类，两个同步方法，其中一个是静态的：

public class TestSynchronized {

    public static synchronized void minus() {
        int count = 5;
        for (int i = 0; i < 5; i++) {
            count--;
            System.out.println(Thread.currentThread().getName() + " - " + count);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
            }
        }
    }

    public synchronized void minus2() {
        int count = 5;
        for (int i = 0; i < 5; i++) {
            count--;
            System.out.println(Thread.currentThread().getName() + " - " + count);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
            }
        }
    }

}

测试调用类如下，静态方法直接用类调用，实例方法由对象来调用：

public class Run {

    public static void main(String[] args) {

        final TestSynchronized test = new TestSynchronized();

        Thread thread1 = new Thread(new Runnable() {

            @Override
            public void run() {
                TestSynchronized.minus();
            }
        });

        Thread thread2 = new Thread(new Runnable() {

            @Override
            public void run() {
                test.minus2();
            }
        });

        thread1.start();
        thread2.start();

    }

}

运行结果：

Thread-1 - 4
Thread-0 - 4
Thread-0 - 3
Thread-1 - 3
Thread-0 - 2
Thread-1 - 2
Thread-0 - 1
Thread-1 - 1
Thread-1 - 0
Thread-0 - 0

可以看到两个线程是交替进行的，也就是说类锁和对象锁是不一样的锁，是互相独立的。
*/